'use strict'

const { storage } = require('../../../../../datadog-core')
const { getNonDDCallSiteFrames } = require('../path-line')
const { getIastContext, getIastStackTraceId } = require('../iast-context')
const overheadController = require('../overhead-controller')
const { SinkIastPlugin } = require('../iast-plugin')
const {
  addVulnerability,
  getVulnerabilityCallSiteFrames,
  replaceCallSiteFromSourceMap
} = require('../vulnerability-reporter')
const { getMarkFromVulnerabilityType } = require('../taint-tracking/secure-marks')
const { SUPPRESSED_VULNERABILITIES } = require('../telemetry/iast-metric')

class Analyzer extends SinkIastPlugin {
  constructor (type) {
    super()
    this._type = type
    this._secureMark = getMarkFromVulnerabilityType(type)
  }

  _isVulnerable (value, context) {
    return false
  }

  _isExcluded (location) {
    return false
  }

  _report (value, context, meta) {
    const evidence = this._getEvidence(value, context, meta)
    this._reportEvidence(value, context, evidence)
  }

  _reportEvidence (value, context, evidence) {
    const callSiteFrames = getVulnerabilityCallSiteFrames()
    const nonDDCallSiteFrames = getNonDDCallSiteFrames(callSiteFrames, this._getExcludedPaths())

    const location = this._getLocation(value, nonDDCallSiteFrames)

    if (!this._isExcluded(location)) {
      const originalLocation = this._getOriginalLocation(location)
      const spanId = context?.rootSpan?.context().toSpanId()
      const stackId = getIastStackTraceId(context)
      const vulnerability = this._createVulnerability(
        this._type,
        evidence,
        spanId,
        originalLocation,
        stackId
      )

      addVulnerability(context, vulnerability, nonDDCallSiteFrames)
    }
  }

  _reportIfVulnerable (value, context, meta) {
    if (this._isVulnerable(value, context) && this._checkOCE(context, value)) {
      this._report(value, context, meta)
      return true
    }
    return false
  }

  _getEvidence (value) {
    return { value }
  }

  _getLocation (value, callSiteFrames) {
    return callSiteFrames[0]
  }

  _getOriginalLocation (location) {
    const locationFromSourceMap = replaceCallSiteFromSourceMap(location)
    const originalLocation = {}

    if (locationFromSourceMap?.path) {
      originalLocation.path = locationFromSourceMap.path
    }

    if (locationFromSourceMap?.line) {
      originalLocation.line = locationFromSourceMap.line
    }

    if (location?.class_name) {
      originalLocation.class = location.class_name
    }

    if (location?.function) {
      originalLocation.method = location.function
    }

    return originalLocation
  }

  _getExcludedPaths () {}

  _isInvalidContext (store, iastContext) {
    return store && !iastContext
  }

  analyze (value, store = storage('legacy').getStore(), meta) {
    const iastContext = getIastContext(store)
    if (this._isInvalidContext(store, iastContext)) return

    this._reportIfVulnerable(value, iastContext, meta)
  }

  analyzeAll (...values) {
    const store = storage('legacy').getStore()
    const iastContext = getIastContext(store)
    if (this._isInvalidContext(store, iastContext)) return

    for (const value of values) {
      if (this._isVulnerable(value, iastContext)) {
        // TODO(BridgeAR): Here are multiple cases that receive a different
        // number of arguments than passed through. Fix those cases.
        if (this._checkOCE(iastContext, value)) {
          this._report(value, iastContext)
        }
        break
      }
    }
  }

  _checkOCE (context) {
    return overheadController.hasQuota(overheadController.OPERATIONS.REPORT_VULNERABILITY, context, this._type)
  }

  _createVulnerability (type, evidence, spanId, location, stackId) {
    if (type && evidence) {
      const _spanId = spanId || 0
      return {
        type,
        evidence,
        location: {
          spanId: _spanId,
          stackId,
          ...location
        },
        hash: this._createHash(this._createHashSource(type, evidence, location))
      }
    }
    return null
  }

  _createHashSource (type, evidence, location) {
    return location ? `${type}:${location.path}:${location.line}` : type
  }

  _createHash (hashSource) {
    let hash = 0
    let offset = 0
    const size = hashSource.length
    for (let i = 0; i < size; i++) {
      hash = ((hash << 5) - hash) + hashSource.charCodeAt(offset++)
    }
    return hash
  }

  _getSuppressedMetricTag () {
    if (!this._suppressedMetricTag) {
      this._suppressedMetricTag = SUPPRESSED_VULNERABILITIES.formatTags(this._type)[0]
    }
    return this._suppressedMetricTag
  }

  _incrementSuppressedMetric (iastContext) {
    SUPPRESSED_VULNERABILITIES.inc(iastContext, this._getSuppressedMetricTag())
  }

  addSub (iastSubOrChannelName, handler) {
    const iastSub = typeof iastSubOrChannelName === 'string'
      ? { channelName: iastSubOrChannelName }
      : iastSubOrChannelName

    super.addSub({ tag: this._type, ...iastSub }, handler)
  }
}

module.exports = Analyzer
